package com.yeskery.nut.application.tomcat;

import com.yeskery.nut.application.NutHttpServlet;
import com.yeskery.nut.application.NutServer;
import com.yeskery.nut.application.NutServerConfigure;
import com.yeskery.nut.core.NutException;
import com.yeskery.nut.util.ClassUtils;
import com.yeskery.nut.websocket.WebSocketConfiguration;
import com.yeskery.nut.websocket.WebSocketServer;
import org.apache.catalina.Context;
import org.apache.catalina.Host;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.Wrapper;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.startup.Tomcat;

import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * 基于Tomcat的HttpServer实现HTTP服务
 * @author sprout
 * @version 1.0
 * 2022-08-05 11:10
 */
public class TomcatNutServer implements NutServer, WebSocketServer {

    /** 日志对象 */
    private static final Logger logger = Logger.getLogger(TomcatNutServer.class.getName());

    /** 默认协议 */
    private static final String DEFAULT_PROTOCOL = "org.apache.coyote.http11.Http11NioProtocol";

    /** WebSocket依赖检查class名称 */
    private static final String WS_DEPEND_CHECK_CLASS_NAME = "org.apache.tomcat.websocket.server.WsServerContainer";

    /** cg-lib依赖检查class名称 */
    private static final String CG_LIB_DEPEND_CHECK_CLASS_NAME = "net.sf.cglib.proxy.Enhancer";

    /** Tomcat WebSocket加载器 */
    private TomcatWebSocketLoader tomcatWebSocketStater;

    /** WebSocket配置对象集合 */
    private Collection<WebSocketConfiguration> webSocketConfigurations;

    /** Tomcat Server对象 */
    private Tomcat tomcat;

    /** Tomcat Server 上下文对象 */
    private Context context;

    /** Tomcat Server 主机对象 */
    private Host host;

    /** 是否已启动 */
    private boolean isStarted;

    /** 是否安全方式启动 */
    protected boolean secure = false;

    @Override
    public void startServer(NutServerConfigure nutServerConfigure) throws Exception {
        tomcatWebSocketStater = new TomcatWebSocketLoaderFactory(
                nutServerConfigure.getNutApplication().getApplicationContext()).buildTomcatWebSocketLoader();
        tomcat = new Tomcat();
        File baseDir = createTempDir("tomcat", nutServerConfigure.getPort());
        tomcat.setBaseDir(baseDir.getAbsolutePath());
        Connector connector = buildServerConnector(nutServerConfigure.getPort());
        tomcat.getService().addConnector(connector);
        tomcat.setConnector(connector);
        tomcat.getHost().setAutoDeploy(false);

        host = tomcat.getHost();
        context = new StandardContext();
        context.setName("");
        context.setPath("");
        File docBase = createTempDir("tomcat-docbase", nutServerConfigure.getPort());
        context.setDocBase(docBase.getAbsolutePath());
        context.addLifecycleListener(new Tomcat.FixContextListener());

        if (enable()) {
            tomcatWebSocketStater.addWsContextListener(context);
        }

        Wrapper nutHttpServlet = context.createWrapper();
        nutHttpServlet.setName("nutHttpServlet");
        nutHttpServlet.setServlet(new NutHttpServlet(nutServerConfigure.getNutApplication(), nutServerConfigure.getServerContext(),
                nutServerConfigure.getDispatcher(), nutServerConfigure.getSessionManager(),
                nutServerConfigure.getServerRequestConfiguration(), this));
        nutHttpServlet.setLoadOnStartup(1);
        nutHttpServlet.setOverridable(true);
        context.addChild(nutHttpServlet);
        context.addServletMappingDecoded("/", "nutHttpServlet");

        host.addChild(context);

        logger.info(getServerStartedTip(nutServerConfigure.getPort(), "Tomcat", secure));
        tomcat.start();
        isStarted = true;
        if (enable()) {
            registerEndpoints(webSocketConfigurations);
        }
        tomcat.getServer().await();
    }

    @Override
    public void checkDependExist() throws Exception {
        if (!ClassUtils.isExistTargetClass(WS_DEPEND_CHECK_CLASS_NAME)) {
            throw new NutException("Tomcat Server Used WebSocket, Such As @WebSocket Annotation, But Not Found " +
                    "Tomcat WebSocket Library, Check Has Been Import tomcat-embed-websocket?");
        }
        if (!ClassUtils.isExistTargetClass(CG_LIB_DEPEND_CHECK_CLASS_NAME)) {
            throw new NutException("Tomcat Server Used WebSocket, Such As @WebSocket Annotation, But Not Found " +
                    "cg-lib Library, Check Has Been Import cglib.cglib-nodep?");
        }
    }

    @Override
    public void registerEndpoints(Collection<WebSocketConfiguration> webSocketConfigurations) {
        if (isStarted) {
            tomcatWebSocketStater.registerEndpoints(context, webSocketConfigurations);
        } else {
            this.webSocketConfigurations = webSocketConfigurations;
        }
    }

    @Override
    public boolean enable() {
        return webSocketConfigurations != null && !webSocketConfigurations.isEmpty();
    }

    @Override
    public void close() throws IOException {
        try {
            if (tomcat != null) {
                tomcat.stop();
            }
            if (context != null) {
                context.stop();
            }
            if (host != null) {
                host.stop();
            }
        } catch (LifecycleException e) {
            logger.logp(Level.SEVERE, TomcatNutServer.class.getName(), "closeServer",
                    "Tomcat Server Close Failure.", e);
        }
    }

    /**
     * 构建连接器对象
     * @param port 端口号
     * @return 连接器对象
     */
    protected Connector buildServerConnector(int port) {
        Connector connector = new Connector(DEFAULT_PROTOCOL);
        connector.setPort(port);
        connector.setProperty("bindOnInit", "false");
        return connector;
    }

    /**
     * 创建临时目录
     * @param prefix 目录前缀
     * @param port 端口号
     * @return 临时目录
     */
    private File createTempDir(String prefix, int port) {
        try {
            File tempDir = File.createTempFile(prefix + ".", "." + port);
            tempDir.delete();
            tempDir.mkdir();
            tempDir.deleteOnExit();
            return tempDir;
        } catch (IOException e) {
            throw new NutException("Unable to create tempDir. java.io.tmpdir is set to " + System.getProperty("java.io.tmpdir"), e);
        }
    }
}
